-
Notifications
You must be signed in to change notification settings - Fork 2.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Minor adjustments to profiler #11376
Conversation
|
size-limit report 📦
|
/** | ||
* Iterates the renders until the render count is reached. | ||
*/ | ||
takeUntilRenderCount( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This behaved in a way that was unintuitive due to the fact that it used the total render count rather than the iterator in its implementation. This isn't currently used, so I'd like us to try using the other methods first before determining if we need to reintroduce this.
If we do reintroduce, I'd propose that instead of thinking about taking each render up to a specific count, that instead this method advances the iterator a certain number of renders from "current". Doing so would prevent weird situations like this:
await Profiled.takeRender()
await Profiled.takeRender()
await Profiled.takeRender()
// This makes no sense since render #2 is now in the past
await Profiled.takeUntilRenderCount(2)
*/ | ||
currentRenderCount(): number; | ||
totalRenderCount(): number; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is now the only function that doesn't rely on the iterator position in its implementation. If possible, we should keep it that way to try and maintain the intent of the original API which should force you to iterate on each render in your tests.
|
||
interface ProfiledComponentOnlyFields<Props, Snapshot> { | ||
// Allows for partial updating of the snapshot by shallow merging the results | ||
setSnapshot: SetSnapshot<Snapshot>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added a setSnapshot
method which allows us now to set a partial snapshot, which gets merged with the full snapshot. I took the name from React's old this.setState()
method in class components, which has similar behavior.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See the comment above - we might also consider the current behaviour, but immer
-wrapped.
// once before we can get a current render. | ||
const currentPosition = iteratorPosition - 1; | ||
|
||
if (currentPosition < 0) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because I'm changing "current render" to mean the render pointing to the most recent takeRender
call, I'm requiring that takeRender
be called at least once. We could just point at the first render if its available, but I felt that to be weird in the following situation:
const current1 = Profiled.getCurrentRender();
const result = await Profiled.takeRender();
// Without requiring `takeRender`, this would be equal to current1
const current2 = Profiled.getCurrentRender();
On the flip side, we could make getCurrentRender
be equal to the iterator position, but this means that it represents the render "after" the last takeRender
call, which also feels weird. This means that calling getCurrentRender
after you've exhausted the render array via takeRender
would point to a render that does not exist.
ProfiledApp.setSnapshot(({ errorCount, errors }) => ({ | ||
errorCount: errorCount + 1, | ||
errors: errors.concat(error), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm almost considering that we get immer
in here for the tests.
That would end up looking like this,
ProfiledApp.setSnapshot(({ errorCount, errors }) => ({ | |
errorCount: errorCount + 1, | |
errors: errors.concat(error), | |
ProfiledApp.updateSnapshot(snapshot => { | |
snapshot.errorCount++; | |
snapshot.errors.push(error) | |
but would still enable something like this
ProfiledApp.updateSnapshot(snapshot => {
return {}
})
which would reset the snapshot to an empty object - which would not be possible with the setSnapshot
notation here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess I'm indifferent here. I didn't consider an alternative library only because it was a small change, but I can see how it would be useful.
FWIW, to reset back to an empty object, I figured you'd just use updateSnapshot
directly since updateSnapshot
does a whole replacement on the snapshot object.
ProfiledApp.updateSnapshot({})
I never intended setSnapshot
to be used for this case, and IMO was the differentiator between the update*
and set*
versions of this function.
The reason I felt like a setSnapshot
would be useful (which you don't see in this PR since this PR contains updates to counts, but would be used in useLoadableQuery
) is cases where you want to record the value from a hook, which might only be part of the whole snapshot.
function Child({ queryRef }) {
const { data } = useReadQuery(queryRef)
// No need for the function callback since I can just update the partial snapshot
ProfiledApp.setSnapshot({ data })
}
// or record the whole object under a specific key without the need for a callback function
ProfiledApp.setSnapshot({ result: useReadQuery(queryRef) })
Again, I'm indifferent, and if adding a dependency on immer
would make this more useful, thats fine.
Thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, I'm just now realizing that produce
in Immer enforces a callback function.
The motivation behind setSnapshot
was to try and avoid the noise of a callback function when I just wanted to record a new value for a particular key. This PR doesn't do a great job of highlighting this use case since updateSnapshot
is mainly used to increment counters, but there are a decent number of cases in the useLoadableQuery
PR that would benefit from the setSnapshot
style.
const result = useReadQuery(queryRef)
ProfiledApp.setSnapshot({ result })
// vs
ProfiledApp.updateSnapshot((snapshot) => ({ ...snapshot, result }))
// or with immer
ProfiledApp.updateSnapshot((snapshot) => {
snapshot.result = result;
})
This is especially useful if you just want to inline everything:
ProfiledApp.setSnapshot({ result: useReadQuery(queryRef) })
I'd like to preserve some way of being able to partial update a key on the snapshot without the use of a callback function if possible to cut down on the noise. I do agree that the immer-style way of doing things is useful for incrementing counters or "mutating" existing values. Perhaps there is a way we can have both?
|
||
interface ProfiledComponentOnlyFields<Props, Snapshot> { | ||
// Allows for partial updating of the snapshot by shallow merging the results | ||
setSnapshot: SetSnapshot<Snapshot>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See the comment above - we might also consider the current behaviour, but immer
-wrapped.
Got a verbal approval from @phryneas, so this is good to merge once tests pass. |
Makes some adjustments to the
profile
andprofileHook
utilities for our tests to better align with the paradigm of those utilities.One of the points of confusion I first had when using this utility is the point in time in which each of the profiled component helper functions execute, specifically the
currentRenderCount
,getCurrentRender
, andtakeUntilRenderCount
methods. These methods operated on the "real" point in time, while the other methods (takeRender
,peekRender
, etc.) all relied on theiteratorPosition
to determine where in the render cycle we were.This confusion led me to use/abuse the API in #11300 that strayed from the original intent of the profiler utility.
To move this utility more in the direction of its original intent, I'm making some updates to the existing API to further clarify this behavior, specifically these changes:
currentRenderCount
tototalRenderCount
.This makes it clear that we are trying to measure the total number of times the profiled component has rendered up to a given point in the test, regardless of iterator position.
getCurrentRender
to return the render at the current iterator position.With this change, "current" is meant to represent the render at the current iterator position, not the latest render in the array.
takeUntilRenderCount
method.For starters, this wasn't currently used anywhere. Secondly, the implementation of this method relied on the total number of renders rather than the iterator count, so trying to use this in the test behaved unpredictably. While I could fix this, I want to see if simply using
await Profiled.takeRender()
the number of times we want to advance is sufficient enough for our tests.Going forward, I propose that if we need a utility that gets us information about the latest render, we use the term "latest" in the name. This makes it clearer that a method with "current" in the name is meant to represent the "render at the current iterator position". I'm holding off on doing that in this PR though as I'd like to try and force us to use methods that rely on the iterator as much as possible to try and avoid abuse of the API. We can revisit in the future to determine if we need this.
One other adjustment I've made to snapshots is that I've introduced a
setSnapshot
method that takes a partial snapshot and does a shallow merge with the full snapshot. Think of this akin to React's oldthis.setState
method in class components where it would shallow merge the object with the current state. I found this to be a bit more ergonomic in cases where I'd just like to update the value of a particular key in the snapshot but don't have to pass a function to do so.